/*
** Copyright (c) 2008 Mobile GCalendarManager
**
** Permission is hereby granted, free of charge, to any person
** obtaining a copy of this software and associated documentation
** files (the "Software"), to deal in the Software without
** restriction, including without limitation the rights to use,
** copy, modify, merge, publish, distribute, sublicense, and/or sell
** copies of the Software, and to permit persons to whom the
** Software is furnished to do so, subject to the following
** conditions:
**
** The above copyright notice and this permission notice shall be
** included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
** OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
** HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
** WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
** OTHER DEALINGS IN THE SOFTWARE.
*/

package au.edu.mq.comp.itec800.mgc.ui.cli.commands;

import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

import org.apache.log4j.Logger;

import au.edu.mq.comp.itec800.mgc.MGC;
import au.edu.mq.comp.itec800.mgc.lib.GAccount;
import au.edu.mq.comp.itec800.mgc.lib.GCalendar;
import au.edu.mq.comp.itec800.mgc.lib.GCalendarManager;
import au.edu.mq.comp.itec800.mgc.ui.cli.ConsoleIO;
import au.edu.mq.comp.itec800.mgc.ui.cli.ICommand;
import au.edu.mq.comp.itec800.mgc.util.CommandUtils;
import au.edu.mq.comp.itec800.mgc.util.MGCUtils;
import au.edu.mq.comp.itec800.mgc.util.StringUtils;
import au.edu.mq.comp.itec800.mgc.util.Utils;

import com.google.gdata.client.GoogleService.InvalidCredentialsException;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;

/**
 * Main-context management class.
 *
 * @author Laurent Malvert [laurent.malvert@students.mq.edu.au]
 * @author Kenfe-Mickael Laventure [kenfe-mickael.laventure@students.mq.edu.au]
 */
public enum MainCommand implements ICommand
{
  add
  {
    @Override
    public boolean  execute(final String ... args)
    {
      boolean       succeeded = false;
      GCalendar     cal       = null;

      try
      {
        cal = MGCUtils.getGAccount().getCalendarManager().
          create(MGCUtils.query(" >> Title: ", 0, args),
                 MGCUtils.query(" >> TimeZone[country/city]: ", 1, args),
                 MGCUtils.query(" >> Visibility[visible/hidden]: ", 2, args),
                 MGCUtils.query(" >> Color[0-22]: ", 3, args),
                 MGCUtils.query(" >> Location: ", 4, args),
                 MGCUtils.query(" >> Description: ", 5, args));
        MGCUtils.getConsole().getIO().output(STATUS1,
                                             Utils.state(cal != null),
                                             cal.getShortId());
      }
      catch (final IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (final ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      if (cal != null)
      {
        MGCUtils.getConsole().getIO().output(STATUS1,
                                             Utils.state(true),
                                             cal.getShortId());
      }
      else
      {
        MGCUtils.getConsole().getIO().output(STATUS1,
                                             Utils.state(false),
                                             "");
      }
      return (succeeded);
    }
  },
  edit
  {
    @Override
    public boolean  execute(final String ... args)
    {
      final GCalendarManager    gcm       = MGCUtils.getGAccount().getCalendarManager();
      boolean                   succeeded = false;

      try
      {
        final List<GCalendar> all = gcm.getCombined();

        for (final String arg : args)
        {
          try
          {
            final int id  = Integer.valueOf(StringUtils.clean(arg));

            if (Utils.isInBound(all, id))
            {
              final GCalendar cal = all.get(id);

              cal.updateName(MGCUtils.query(" >> Title: "));
              cal.updateTimeZone(MGCUtils.query(" >> TimeZone [country/city]: "));
              cal.updateVisibility(MGCUtils.query(" >> Visibility[visible/hidden]: "));
              cal.updateColor(MGCUtils.query(" >> Color[0-22]: "));
              cal.updateLocation(MGCUtils.query(" >> Location: "));
              cal.updateDescription(MGCUtils.query(" >> Description: "));
              MGCUtils.getConsole().getIO().output(STATUS1,
                                                   Utils.state(cal.update() != null),
                                                   cal.getShortId());
            }
          }
          catch (final NumberFormatException e)
          {
            logger.warn(e.getMessage(), e);
          }
          catch (final IOException e)
          {
            logger.error(e.getMessage(), e);
          }
          catch (final ServiceException e)
          {
            logger.error(e.getMessage(), e);
          }
        }
        succeeded = true;
      }
      catch (final IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (final ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      return (succeeded);
    }
  },
  help
  {
    @Override
    public boolean  execute(final String ... args)
    {
      final StringBuffer  h = new StringBuffer();

      /*
       * TODO:
       * reflectively find contexts and command here and will the
       * stringbuffer to display the help message
       */
      MGCUtils.getConsole().getIO().output(h.toString());
      return (true);
    }
  },
  list
  {
    @Override
    public boolean  execute(final String ... args)
    {
      boolean             succeeded = false;
      final String        arg       = StringUtils.getValidArg(args, 0).toLowerCase();
      final StringBuffer  out       = new StringBuffer();

      try
      {
        final List<GCalendar>   all = MGCUtils.getGAccount().getCalendarManager().getCombined();
        int                     idx = -1;

        for (final GCalendar cal : all)
        {
          ++idx;
          if ((arg.equals("own") && (cal.isOwn() == false)) ||
              (arg.equals("subscribed") && (cal.isOwn())))
            continue;
          out.append(String.format(" %1s%1s %2d. %s\n",
                                   cal.isOwn() ? "*" : "" ,
                                   cal.isHidden() ? '-' : '+',
                                   idx,
                                   cal.getName()));
          //TODO: make a paging system so the cmd stay scrollable
        }
        MGCUtils.getConsole().getIO().output(out.toString());
        succeeded = true;
      }
      catch (final IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (final ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      return (succeeded);
    }
  },
  noop
  {
    @Override
    public boolean  execute(final String ... args)
    {
      return (true);  //  does nothing but does it well
    }
  },
  update
  {
    @Override
    public boolean  execute(final String ... args)
    {
      try
      {
        MGCUtils.getGAccount().getCalendarManager().update();
      }
      catch (IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      return (true);
    }
  },
  remove
  {
    @Override
    public boolean  execute(final String ... args)
    {
      final GCalendarManager    gcm       = MGCUtils.getGAccount().getCalendarManager();
      boolean                   succeeded = false;
      final StringBuffer        out       = new StringBuffer(512);

      try
      {
        final List<GCalendar> all       = gcm.getCombined();
        final List<GCalendar> toRemove  = new ArrayList<GCalendar>();

        for (final String arg : args)
        {
          try
          {
            final int           id  = Integer.valueOf(StringUtils.clean(arg));

            if (Utils.isInBound(all, id))
            {
              final GCalendar cal = all.get(id);

              out.append(String.format(STATUS2,
                                       Utils.state(succeeded = GCalendarManager.delete(cal)),
                                       arg,
                                       cal.getName()));
              if (succeeded == true)
              {
                toRemove.add(cal);
              }
            }
          }
          catch (final NumberFormatException e)
          {
            logger.warn(e.getMessage(), e);
          }
          catch (final IOException e)
          {
            logger.error(e.getMessage(), e);
          }
          catch (final ServiceException e)
          {
            logger.error(e.getMessage(), e);
          }
        }
        all.removeAll(toRemove);
        succeeded = true;
        MGCUtils.getConsole().getIO().output(out.toString().replaceAll("%", "%%"));
      }
      catch (final IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (final ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      return (succeeded);
    }
  },
  view
  {
    @Override
    public boolean  execute(final String ... args)
    {
      final GCalendarManager    gcm       = MGCUtils.getGAccount().getCalendarManager();
      boolean                   succeeded = false;

      try
      {
        final List<GCalendar> all   = gcm.getCombined();
        final int             size  = all.size();
        int                   index = 0;

        for (final String arg : args)
        {
          final StringBuffer  view  = new StringBuffer(512);

          try
          {
            final int           id  = Integer.valueOf(StringUtils.clean(arg));

            if (Utils.isInBound(all, id))
            {
              final GCalendar   cal = all.get(id);

              view.append(" >> Title: ");
              view.append(cal.getName());
              view.append("\n >> TimeZone: ");
              view.append(cal.getTimezone());
              view.append("\n >> Visibility: ");
              view.append(cal.isHidden() ? "hidden" : "visible");
              view.append("\n >> Color: ");
              view.append(cal.getColorName());
              view.append("\n >> Location: ");
              view.append(cal.getLocation());
              view.append("\n >> Description: ");
              view.append(cal.getDescription());
              view.append("\n\n");
            }
          }
          catch (final NumberFormatException e)
          {
            logger.warn(e.getMessage(), e);
          }
          MGCUtils.getConsole().getIO().output(view.toString());
          if (++index < size)
          {
            MGCUtils.getConsole().getIO().pause(PAUSE);
          }
        }
      }
      catch (final IOException e)
      {
        logger.error(e.getMessage(), e);
      }
      catch (final ServiceException e)
      {
        logger.error(e.getMessage(), e);
      }
      return (succeeded);
    }
  },
  exit
  {
    @Override
    public boolean    execute(final String ... args)
    {
      MGC.getInstance().getUI().stop();
      return (true);
    }
  },
  subscribe
  {
    @Override
    public boolean    execute(final String ... args)
    {
      final GCalendarManager  gcm = MGCUtils.getGAccount().getCalendarManager();
      final StringBuffer      out = new StringBuffer(512);

      for (final String calId : args)
      {
        final String  sCalId  = StringUtils.clean(calId);

        if (sCalId.length() > 0)
        {
          GCalendar cal = null;

          try
          {
            if ((cal = gcm.subscribe(sCalId)) != null)
            {
              gcm.getCombined().add(cal);
            }
          }
          catch (final IOException e)
          {
            logger.error(e.getMessage(), e);
          }
          catch (final ServiceException e)
          {
            logger.error(e.getMessage(), e);
          }
          out.append(String.format(STATUS1, Utils.state(cal != null), sCalId));
        }
      }
      MGCUtils.getConsole().getIO().output(out.toString().replaceAll("%", "%%"));
      return (true);
    }
  },
  unsubscribe
  {
    @Override
    public boolean    execute(final String ... args)
    {
      return (remove.execute(args));
    }
  },
  user
  {
    private static final int  RETRIES = 5;


    private boolean canRetry(final Exception e)
    {
      logger.warn(e.getMessage(), e);
      return ((e instanceof InvalidCredentialsException) ||
              (e instanceof AuthenticationException));
    }

    public GAccount login(final String ... args)
    {
      return (login(StringUtils.isValid(0, args) ? args[0] : null,
                    StringUtils.isValid(1, args) ? args[1] : null));
    }

    public GAccount login()
    {
      return (login(null, null));
    }

    public GAccount login(final String user, final String pass)
    {
      final ConsoleIO io        = MGCUtils.getConsole().getIO();
      final int       retries   = (pass == null) ? RETRIES : 1;
      boolean         loggedin  = false;
      int             tries     = 0;
      GAccount        ga        = null;

      while ((loggedin == false) && (tries < retries))
      {
        try
        {
          ga        = new GAccount((user != null) ? user : io.input("username: "));
          loggedin  = ga.authenticate((pass != null) ? pass : io.password("password: "));
        }
        catch (final NullPointerException e)
        {// can be thrown by java.io.Console.readPassword() with CTRL-C
          tries = retries;
        }
        catch (final IllegalArgumentException e)
        {
          tries = canRetry(e) ? (tries + 1) : retries;
        }
        catch (final ServiceException e)
        {
          tries = canRetry(e) ? (tries + 1) : retries;
        }
      }
      return (loggedin ? ga : null);
    }

    @Override
    public boolean    execute(final String ... args)
    {
      boolean         succeeded = false;
      GAccount        ga        = null;

      if ((args != null) && (args.length > 0))
      { // do we have a username ?
        if (MGCUtils.getMGC().getGoogleAccounts().containsKey(args[0]))
        { // account exists ?
          ga = MGCUtils.getMGC().getGoogleAccounts().get(args[0]);
        }
        else
        { // if no then try to log in with given username
          if ((ga = login(args)) != null)
          {
            MGCUtils.getMGC().getGoogleAccounts().put(ga.getUser(), ga);
          }
        }
      }
      else
      { // if no then try to log in with given user info
        if ((ga = login()) != null)
        {
          MGCUtils.getMGC().getGoogleAccounts().put(ga.getUser(), ga);
        }
      }
      if (ga != null)
      { // switching to existing or successfully created account
        MGCUtils.getMGC().setActiveAccount(ga);
        // getMGC().setContext(Context.main);
      }
      else
      {
        MGCUtils.getConsole().getIO().output("[ERROR] could not switch to given user.\n");
      }
      return (succeeded);
    }
  },

  /*
   * short forms / aliases
   */
  a (add),
  e (edit),
  h (help),
  l (list),
  r (remove),
  s (subscribe),
  u (user),
  v (view),
  x (exit);

  private static final Logger               logger    = Logger.getLogger(MainCommand.class);
  private static final EnumSet<MainCommand> remote    = EnumSet.of(add,
                                                                   edit,
                                                                   list,
                                                                   remove,
                                                                   subscribe,
                                                                   unsubscribe,
                                                                   view);
  private static final String               STATUS1   = " > [%8s] %s\n".intern();
  private static final String               STATUS2   = " > [%8s] %2s - %s\n".intern();
  private static final String               PAUSE     = "press [ENTER] to continue".intern();

  private String                            hlp       = "";
  private ICommand                          aliases[] = null;
  private ICommand                          master    = null;


  private MainCommand()
  {
    this(null, null);
  }

  private MainCommand(final String hlp)
  {
    this(hlp, null);
  }

  private MainCommand(final ICommand master)
  {
    this(null, master);
  }

  private MainCommand(final String hlp, final ICommand master)
  {
    this.master = ((master != null) && (master.name().equals(name()) == false)) ? master : null;
    this.hlp    = (hlp != null) ? hlp : ((this.master != null) ? this.master.help() : "");
  }

  /* (non-Javadoc)
   * @see au.edu.mq.comp.itec800.mgc.ui.cli.ICommand#execute(java.lang.String[])
   *
   * Executes a command or forwards it to the master command if
   * we're dealing with an alias.
   */
  public boolean          execute(final String ... args)
  {
    return (isAlias() ? getMaster().execute(args) : false);
  }

  public boolean          needsAuthentication()
  {
    return (remote.contains(this) ||
            (isAlias() && getMaster().needsAuthentication()));
  }

  public String           getHelp()
  {
    return (hlp);
  }

  public ICommand[]       getAliases()
  {
    if (aliases == null)
    {
      final List<MainCommand>  alist =
        new ArrayList<MainCommand>(MainCommand.values().length);

      for (final MainCommand cmd : MainCommand.values())
      {
        if (cmd.isAlias() && cmd.getMaster().name().equals(name()))
        {
          alist.add(cmd);
        }
      }
      alist.toArray(aliases = new MainCommand[alist.size()]);
    }
    return (aliases);
  }

  public ICommand         getMaster()
  {
    return (master);
  }

  public boolean          isAlias()
  {
    return (CommandUtils.isAlias(this));
  }

  public ICommand         getTopMaster()
  {
    return (CommandUtils.getTopMaster(this));
  }

  public String           help()
  {
    return ("");
  }
}
